본 게시글의 원문은 Yann Debray의 MATLAB with Python Book 입니다. 해당 책은 MIT 라이센스를 따르기 때문에 개인적으로 번역하여 재배포 합니다. 본 포스팅에는 추후 유료 수익을 위한 광고가 부착될 수도 있습니다.

MIT License

Copyright (c) 2023 Yann Debray

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

본 게시물에서 활용된 소스 코드들은 모두 Yann Debray의 GitHub Repo에서 확인할 수 있습니다.

세 번째 시나리오에서는 Python에서 Simulink를 호출하는 역방향 문제를 살펴봅니다. 이 시나리오는 DevOps 엔지니어가 테스트 및 시뮬레이션 자동화를 할 때 자주 접하게 되며, 종종 CI/CD 프로세스의 일환으로 발생합니다.

이 시나리오와 다음 시나리오에서는 도로 서스펜션 모델을 사용할 것입니다.

첫 번째 단계는 MATLAB 함수를 사용하여 시뮬레이션을 래핑하는 것입니다. 이 함수는 시뮬레이션 입력 객체를 사용하여 모델 매개변수(이 경우 차체 질량)를 업데이트할 수 있습니다. 다음으로, sim 명령을 사용하여 일괄 시뮬레이션을 실행해야 합니다. 그런 다음 MATLAB 및/또는 Python에서 시뮬레이션 결과를 후처리할 수 있습니다.

MATLAB 함수:

function res = sim_the_model(args)
% 지정된 매개변수로 Simulink 모델을 시뮬레이션하는 유틸리티 함수
% 
% 입력:
%    StopTime: 시뮬레이션 종료 시간, 기본값은 nan
%    TunableParameters:
%       필드가 조정 가능한 참조 워크스페이스 변수이며 시뮬레이션에 사용할 값을 가진 구조체.
%    
%    위 입력에 대해 nan 또는 빈 값은 시뮬레이션이 모델에 설정된 기본값으로 실행되어야 함을 나타냅니다.
% 
% 출력:
%    res: 기록된 신호의 시간 및 데이터 값을 가진 구조체.

arguments
    args.StopTime (1,1) double = nan
    args.TunableParameters = []
end

    %% SimulationInput 객체 생성
    si = Simulink.SimulationInput('suspension_3dof');
    %% SimulationInput 객체에 StopTime 로드
    if ~isnan(args.StopTime)
        si = si.setModelParameter('StopTime', num2str(args.StopTime));
    end
    
    %% 지정된 조정 가능한 매개변수를 시뮬레이션 입력 객체에 로드
    if isstruct(args.TunableParameters) 
        tpNames = fieldnames(args.TunableParameters);
        for itp = 1:numel(tpNames)
            tpn = tpNames{itp};
            tpv = args.TunableParameters.(tpn);
            si = si.setVariable(tpn, tpv);
        end
    end

    %% sim 호출
    so = sim(si);
    
    %% 시뮬레이션 결과 추출
    % 기록된 신호의 시간 및 데이터 값을 구조체에 패키징
    res = extractResults(so,nan);

end % sim_the_model_using_matlab_runtime

function res = extractResults(so, prevSimTime)
    % 기록된 신호의 시간 및 데이터 값을 구조체에 패키징
    ts = simulink.compiler.internal.extractTimeseriesFromDataset(so.logsout);
    for its=1:numel(ts)
        if isfinite(prevSimTime)
            idx = find(ts{its}.Time > prevSimTime);
            res.(ts{its}.Name).Time = ts{its}.Time(idx);
            res.(ts{its}.Name).Data = ts{its}.Data(idx);
        else
            res.(ts{its}.Name).Time = ts{its}.Time;
            res.(ts{its}.Name).Data = ts{its}.Data;
        end
    end
end

function figHndl = plot_results(res, plotTitle)
%PLOT_RESULTS call_sim_the_model의 결과를 플로팅

figHndl = figure; hold on; cols = colororder;


plot(res{1}.vertical_disp.Time, res{1}.vertical_disp.Data, 'Color', cols(1,:), ...
    'DisplayName', '수직 변위: 기본 Mb 값으로 첫 번째 시뮬레이션');
plot(res{2}.vertical_disp.Time, res{2}.vertical_disp.Data, 'Color', cols(2,:), ...
    'DisplayName', '수직 변위: 새로운 Mb 값으로 두 번째 시뮬레이션');


hold off; grid; 

title(plotTitle,'Interpreter','none');
set(get(gca,'Children'),'LineWidth',2);
legend('Location','southeast');

end

Python 코드:

import matlab.engine

mle = matlab.engine.start_matlab()  # MATLAB 엔진 시작

# 두 번의 sim_the_model 호출 결과를 저장할 res 리스트 할당
res = [0]*2
# 첫 번째 시뮬레이션: 기본 매개변수 값 사용: Mb = 1200 Kg
res[0] = mle.sim_the_model('StopTime', 30)

# 두 번째 시뮬레이션: 조정 가능한 매개변수의 새로운 값 사용
tunableParams = {
    # 차체 질량에 대한 새로운 매개변수 사용: Mb = 5000 Kg
    'Mb': 5000.0
}
res[1] = mle.sim_the_model('StopTime', 30, 'TunableParameters', tunableParams)

# MATLAB으로 콜백하여 결과 플로팅
mle.plot_results(res, "MATLAB 엔진을 사용한 sim_the_model의 결과")

input("MATLAB 그림을 닫고 종료하려면 Enter 키를 누르세요...")
mle.quit()  # MATLAB 엔진 종료

이 코드는 다음과 같은 MATLAB 플롯을 생성합니다:

네 번째 시나리오에서는 Simulink 모델을 Python 기반의 프로덕션 시스템에 배포하는 것이 목표입니다.

여기서 단계는 동일합니다. 유일한 차이점은 모델을 배포용으로 구성해야 한다는 것입니다. 모든 Simulink 모델이 배포 가능한 것은 아니기 때문입니다. 따라서 시뮬레이션 입력 객체를 호출하기 전에 다음 줄을 추가하여 변환해야 합니다:

si = simulink.compiler.configureForDeployment(si);

그 다음 단계는 MATLAB 함수를 Python 패키지로 컴파일하는 것입니다:

appFile = which('sim_the_model2');
outDir = fullfile(origDir,'sim_the_model2_python_package_installer');
compiler.build.pythonPackage(appFile, ...
        'PackageName','sim_the_model2', ...
        'OutputDir',outDir);

마지막으로, Python 패키지를 설치하고 다음과 같이 호출해야 합니다:

import sim_the_model2
import matplotlib.pyplot as plt

# sim_the_model 패키지 초기화
mlr = sim_the_model2.initialize()

# 두 번의 sim_the_model 호출 결과를 저장할 res 리스트 할당
res = [0]*2
# 첫 번째 시뮬레이션: 기본 매개변수 값 사용: Mb = 1200 Kg
res[0] = mlr.sim_the_model2('StopTime', 30)

# 두 번째 시뮬레이션: 조정 가능한 매개변수의 새로운 값 사용
tunableParams = {
    'Mb': 5000.0   # 차체 질량에 대한 새로운 매개변수 Kg
}
res[1] = mlr.sim_the_model2('StopTime', 30, 'TunableParameters', tunableParams)


# 결과 플로팅
cols = plt.rcParams['axes.prop_cycle'].by_key()['color']
fig, ax = plt.subplots(1, 1, sharex=True)
ax.plot(res[0]['vertical_disp']['Time'], res[0]['vertical_disp']['Data'], color=cols[0],
        label="수직 변위: 기본 차체 질량 Mb으로 첫 번째 시뮬레이션")
ax.plot(res[1]['vertical_disp']['Time'], res[1]['vertical_disp']['Data'],
        color=cols[1], label="수직 변위: 업데이트된 차체 질량 Mb으로 두 번째 시뮬레이션")


ax.grid()
lg = ax.legend(fontsize='x-small')
lg.set_draggable(True)
ax.set_title("MATLAB 런타임을 사용한 sim_the_model의 결과")
plt.show()

mlr.terminate()  # MATLAB 런타임 종료

이 코드는 다음과 같은 Python 플롯을 생성합니다: